;; This procedure recalculates the coordinates of the Mandelbrot set
;; that are to be drawn. If the user clicked the left-hand mouse
;; button (Event.press = 1) then the coordinates are zoomed in by a
;; factor of info.zoom, centred on the click position. If the user
;; clicked the right-hand mouse button (Event.press = 4) then the
;; coordinates are zoomed out by a factor of 1.0 / info.zoom. If any
;; other button was clicked (i.e. the middle one) then the zoom is
;; set to 1.0, but the centre of the coordinates is set at the
;; mouse-click location.
PRO fractalMoveAndZoom, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  rRangeCurrent = info.rMax - info.rMin
  iRangeCurrent = info.iMax - info.iMin

  ;; Zoom in if it was the left-hand button, and out if
  ;; it was the right-hand button, but first check that
  ;; Event contains the "press" element. If it doesn't
  ;; then set zoom = 1.0.
  tagNames = TAG_NAMES(Event)
  IF MAX(STRCMP(tagNames, 'press', /FOLD_CASE)) NE 0 THEN BEGIN
    CASE Event.press OF
      1: zoom = info.zoom
      4: zoom = 1.0 / info.zoom
    ELSE: zoom = 1.0
    ENDCASE

    rRangeNew = rRangeCurrent / zoom
    iRangeNew = iRangeCurrent / zoom

    rClicked = info.rMin + ((Event.x / DOUBLE(info.imageSize[0])) * rRangeCurrent)
    iClicked = info.iMin + ((Event.y / DOUBLE(info.imageSize[1])) * iRangeCurrent)

    info.rMin = rClicked - (rRangeNew / 2.0)
    info.rMax = info.rMin + rRangeNew
    info.iMin = iClicked - (iRangeNew / 2.0)
    info.iMax = info.iMin + iRangeNew

    *info.theFractal -> Change, rMin=info.rMin, rMax=info.rMax, iMin=info.iMin, iMax=info.iMax

    IF info.viewIter EQ 1 THEN BEGIN
      FOR i=0, info.numIter-1 DO BEGIN
        *info.theFractal -> Iterate
        *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
      ENDFOR
    ENDIF ELSE BEGIN
      *info.theFractal -> Iterate, info.numIter
      *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
    ENDELSE

  ENDIF

  ;; Send the info structure back in the uvalue of the button widget.
  WIDGET_CONTROL, Event.top, SET_UVALUE=info, /NO_COPY

END


;; This procedure redraws the existing fractal on the screen. This
;; is necessary because if the window is covered by another window,
;; or it is minimised, then the contents are wiped away.
PRO fractalRedraw, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef

  ;; Send the info structure back in the uvalue of the button widget.
  WIDGET_CONTROL, Event.top, SET_UVALUE=info, /NO_COPY

END


;; This procedure iterates the existing fractal by the number of
;; iterations specified in info.extraIter. If info.viewIter is 1
;; then the screen is updated after each iteration. If set to 0
;; then the screen is updated after all the iterations.
PRO fractalIterateMore, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  IF info.viewIter EQ 1 THEN BEGIN
    FOR i=0, info.extraIter-1 DO BEGIN
      *info.theFractal -> Iterate
      *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
    ENDFOR
  ENDIF ELSE BEGIN
    *info.theFractal -> Iterate, info.extraIter
    *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
  ENDELSE

  ;; Send the info structure back in the uvalue of the button widget.
  WIDGET_CONTROL, Event.top, SET_UVALUE=info, /NO_COPY

END


;; This procedure displays a little information.
PRO fractalAbout, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  result = DIALOG_MESSAGE('Written by David Platten, May 2013', /INFORMATION, TITLE='About fractal')

END


;; This procedure displays the fractalInstructions.txt file in a window.
;; The file contains instructions on how to use the program.
PRO fractalInstructions, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  XDISPLAYFILE, 'fractalInstructions.txt', TITLE='Fractal instructions', WIDTH=60

END


;; This procedure calculates the new coordinate area after a window resize.
;; It then resizes the window, recalculates the fractal and displays it.
PRO fractalResizeWindow, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  IF Event.x EQ info.imageSize[0] THEN BEGIN
    currentIrange = info.iMax - info.iMin
    info.iMin = info.iMax - (currentIrange * DOUBLE(Event.y) / DOUBLE(info.imageSize[1]))
    info.imageSize[1] = Event.y
  ENDIF ELSE BEGIN
    IF Event.y EQ info.imageSize[1] THEN BEGIN
      currentRrange = info.rMax - info.rMin
      info.rMax = info.rMin + (currentRrange * DOUBLE(Event.x) / DOUBLE(info.imageSize[0]))
      info.imageSize[0] = Event.x
    ENDIF ELSE BEGIN
      currentIrange = info.iMax - info.iMin
      info.iMin = info.iMax - (currentIrange * DOUBLE(Event.y) / DOUBLE(info.imageSize[1]))

      currentRrange = info.rMax - info.rMin
      info.rMax = info.rMin + (currentRrange * DOUBLE(Event.x) / DOUBLE(info.imageSize[0]))

      info.imageSize = [Event.x, Event.y]
    ENDELSE
  ENDELSE

  wDraw = WIDGET_INFO(Event.top, FIND_BY_UNAME='drawWidget')
  wBase = WIDGET_INFO(Event.top, FIND_BY_UNAME='tlb')

  WIDGET_CONTROL, wBase, XSIZE=info.imageSize[0], YSIZE=info.imageSize[1]
  WIDGET_CONTROL, wDraw, XSIZE=info.imageSize[0], YSIZE=info.imageSize[1]

  *info.oViewRef -> SetProperty, VIEWPLANE_RECT = [0, 0, [info.imageSize[0], info.imageSize[1]]]

  *info.theFractal -> Change, rMin=info.rMin, rMax=info.rMax, iMin=info.iMin, iMax=info.iMax, imageSize=info.imageSize

  IF info.viewIter EQ 1 THEN BEGIN
    FOR i=0, info.numIter-1 DO BEGIN
      *info.theFractal -> Iterate
      *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
    ENDFOR
  ENDIF ELSE BEGIN
    *info.theFractal -> Iterate, info.numIter
    *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
  ENDELSE

  ;; Send the info structure back in the uvalue of the button widget.
  WIDGET_CONTROL, Event.top, SET_UVALUE=info, /NO_COPY

END


;; This procedure saves the currently displayed fractal as a bitmap.
;; I think it may save as postscript if you're using Linux or MacOS.
PRO fractalSave, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  sFile = cgPickfile(WRITE=WRITE)

  oClipboard = OBJ_NEW('IDLgrClipboard', DIMENSIONS=[info.imageSize[0], info.imageSize[1]])
  oClipboard -> Draw, *info.oViewRef, Filename=sFile, VECTOR=0, POSTSCRIPT=0

  OBJ_DESTROY, oClipboard

  ;; Send the info structure back in the uvalue of the button widget.
  WIDGET_CONTROL, Event.top, SET_UVALUE=info, /NO_COPY

END


;; This procedure resets the fractal back to the default set of
;; coordinates, iterates and then draws it in the centre of the
;; screen.
PRO fractalReset, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  ;; Reset the r and i ranges
  aspectRatio = DOUBLE(info.imageSize[0]) / DOUBLE(info.imageSize[1])
  IF aspectRatio GT 1.0 THEN BEGIN
    rCentre = (-2.0D + 0.5D) / 2.0
    rRange  = 2.5D * aspectRatio
    info.rMin = rCentre - (rRange / 2.0)
    info.rMax = info.rMin + rRange
    info.iMin = -1.25D
    info.iMax = 1.25D
  ENDIF ELSE BEGIN
    iCentre = 0.0D
    iRange  = 2.5D / aspectRatio
    info.iMin = iCentre - (iRange / 2.0)
    info.iMax = info.iMin + iRange
    info.rMin = -2.0D
    info.rMax = 0.5D
  ENDELSE

  *info.theFractal -> Reset

  IF info.viewIter EQ 1 THEN BEGIN
    FOR i=0, info.numIter-1 DO BEGIN
      *info.theFractal -> Iterate
      *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
    ENDFOR
  ENDIF ELSE BEGIN
    *info.theFractal -> Iterate, info.numIter
    *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
  ENDELSE

  ;; Send the info structure back in the uvalue of the button widget.
  WIDGET_CONTROL, Event.top, SET_UVALUE=info, /NO_COPY

END


;; This procedure cleans up the objects and widgets
;; and then exits the routine.
PRO fractalExit, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  ;; Free up the pointers that are stored in the info structure
  PTR_FREE, info.oDrawRef, info.oImageRef, info.oViewRef, info.oPaletteRef, info.theFractal

  ;; Destroy the widgets
  WIDGET_CONTROL, Event.top, /DESTROY

  ;; Exit the program
  EXIT, /NO_CONFIRM

END


;; This procedure allows the user to specify various options.
PRO fractalOptions, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  ;; Change max iterations by changing info.numIter
  desc = [ $
          '1, BASE,, COLUMN, FRAME', $
          '1, BASE,, ROW', $
          '2, INTEGER,' + STRING(info.numIter) + ', LABEL_LEFT=Maximum iterations:, WIDTH=5, TAG=numIter', $
          '1, BASE,, ROW', $
          '2, INTEGER,' + STRING(info.extraIter) + ', LABEL_LEFT=Extra iterations:, WIDTH=5, TAG=extraIter', $
          '1, BASE,, ROW', $
          '2, INTEGER,' + STRING(info.colourTable) + ', LABEL_LEFT=Colour table (0 to ' + STRING(info.numOfCT-1, FORMAT='(I0)') + '):, WIDTH=5, TAG=colourTable', $
          '1, BASE,, ROW', $
          '2, BUTTON, No|Yes, ROW, TAG=reverseCT, EXCLUSIVE, LABEL_LEFT=Reverse the colours?, SET_VALUE=' + STRING(info.reverseCT), $
          '1, BASE,, ROW', $
          '2, FLOAT,' + STRING(info.zoom) + ', LABEL_LEFT=Zoom increment:, WIDTH=8, TAG=zoom', $
          '1, BASE,, ROW', $
          '2, BUTTON, No|Yes, ROW, TAG=viewIter, EXCLUSIVE, LABEL_LEFT=View each iteration?, SET_VALUE=' + STRING(info.viewIter), $
          '1, BASE,, ROW', $
          '0, BUTTON, OK, QUIT, TAG=OK', $
          '2, BUTTON, Cancel, QUIT' $
         ]

  desc = [desc]

  ;; Display the form
  form = CW_FORM(desc,TITLE='Fractal options', TAB_MODE=1)

  ;; If the user clicked on "OK" then
  IF form.OK EQ 1 THEN BEGIN

    ;; Set the option for viewing iterations
    info.viewIter = form.viewIter

    ;; Set the option for extra iterations
    info.extraIter = form.extraIter

    ;; Update the zoom level
    info.zoom = form.zoom

    ;; Update the palette using the chosen colour table
    info.colourTable = form.colourTable

    ;; Update the reversed colour table option
    info.reverseCT = form.reverseCT

    ;; Load either a standard colour table or Brewer colour table
    ;; and reverse the colours if requested by the user
    IF info.brewerFile THEN BEGIN

      IF info.reverseCT THEN BEGIN
        ;; Read the RGB values of the required colour table into rgbValues
        LOADCT, info.colourTable, FILE=info.brewerFile, RGB_TABLE=rgbValues

        ;; Set the red, green and blue values to the reversed values
        *info.oPaletteRef -> SetProperty, RED_VALUES   = REVERSE(rgbValues[*,0])
        *info.oPaletteRef -> SetProperty, GREEN_VALUES = REVERSE(rgbValues[*,1])
        *info.oPaletteRef -> SetProperty, BLUE_VALUES  = REVERSE(rgbValues[*,2])
      ENDIF ELSE BEGIN
        ;; Load the colour table into the oPalette
        *info.oPaletteRef -> LoadCT, info.colourTable, FILE=info.brewerFile
      ENDELSE

    ENDIF ELSE BEGIN

      IF info.reverseCT THEN BEGIN
        ;; Read the RGB values of the required colour table into rgbValues
        LOADCT, info.colourTable, RGB_TABLE=rgbValues

        ;; Set the red, green and blue values to the reversed values
        *info.oPaletteRef -> SetProperty, RED_VALUES   = REVERSE(rgbValues[*,0])
        *info.oPaletteRef -> SetProperty, GREEN_VALUES = REVERSE(rgbValues[*,1])
        *info.oPaletteRef -> SetProperty, BLUE_VALUES  = REVERSE(rgbValues[*,2])
      ENDIF ELSE BEGIN
        ;; Load the colour table into the oPalette
        *info.oPaletteRef -> LoadCT, info.colourTable
      ENDELSE

    ENDELSE


    ;; Iterate the existing fractal some more if the form's
    ;; numIter is greater than the number of iterations
    ;; carried out so far.
    IF form.numIter GT info.numIter THEN BEGIN
      ;; More iterations are asked for than have been carried out on the
      ;; existing fractal until the new number of iterations is reached
      IF info.viewIter EQ 1 THEN BEGIN
        FOR i=0, (form.numIter - info.numIter) - 1 DO BEGIN
          *info.theFractal -> Iterate
          *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
        ENDFOR
      ENDIF ELSE BEGIN
        *info.theFractal -> Iterate, (form.numIter - info.numIter)
        *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
      ENDELSE

      ;; Update the maximum number of iterations in info.numIter
      info.numIter = form.numIter

    ENDIF ELSE BEGIN

      IF form.numIter LT info.numIter THEN BEGIN
        ;; Less iterations are asked for than the current number, so
        ;; recalculate the fractal from scratch using this updated value.
        *info.theFractal -> Change, rMin=info.rMin, rMax=info.rMax, iMin=info.iMin, iMax=info.iMax

        ;; Update the maximum number of iterations and recalculate the
        ;; fractal using this revised number.
        info.numIter = form.numIter

        IF info.viewIter EQ 1 THEN BEGIN
          FOR i=0, info.numIter - 1 DO BEGIN
            *info.theFractal -> Iterate
            *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
          ENDFOR
        ENDIF ELSE BEGIN
          *info.theFractal -> Iterate, info.numIter
          *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
        ENDELSE

      ENDIF ELSE BEGIN
        ;; Just redraw the fractal - there's no need to recalculate it
        *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
      ENDELSE
    ENDELSE

  ENDIF

  ;; Send the info structure back in the uvalue of the button widget.
  WIDGET_CONTROL, Event.top, SET_UVALUE=info, /NO_COPY

END


;; This procedure changes the colour table to the next available
;; one. It cycles back to the first one the last option has been
;; reached.
PRO fractalCycleCT, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  ;; Add 1 to the current colour table
  info.colourTable += 1
  IF info.colourTable EQ info.numOfCT THEN info.colourTable = 0


  ;; Load either a standard colour table or Brewer colour table
  ;; and reverse the colours if requested by the user
  IF info.brewerFile THEN BEGIN

    IF info.reverseCT THEN BEGIN
      ;; Read the RGB values of the required colour table into rgbValues
      LOADCT, info.colourTable, FILE=info.brewerFile, RGB_TABLE=rgbValues

      ;; Set the red, green and blue values to the reversed values
      *info.oPaletteRef -> SetProperty, RED_VALUES   = REVERSE(rgbValues[*,0])
      *info.oPaletteRef -> SetProperty, GREEN_VALUES = REVERSE(rgbValues[*,1])
      *info.oPaletteRef -> SetProperty, BLUE_VALUES  = REVERSE(rgbValues[*,2])
    ENDIF ELSE BEGIN
      ;; Load the colour table into the oPalette
      *info.oPaletteRef -> LoadCT, info.colourTable, FILE=info.brewerFile
    ENDELSE

  ENDIF ELSE BEGIN

    IF info.reverseCT THEN BEGIN
      ;; Read the RGB values of the required colour table into rgbValues
      LOADCT, info.colourTable, RGB_TABLE=rgbValues

      ;; Set the red, green and blue values to the reversed values
      *info.oPaletteRef -> SetProperty, RED_VALUES   = REVERSE(rgbValues[*,0])
      *info.oPaletteRef -> SetProperty, GREEN_VALUES = REVERSE(rgbValues[*,1])
      *info.oPaletteRef -> SetProperty, BLUE_VALUES  = REVERSE(rgbValues[*,2])
    ENDIF ELSE BEGIN
      ;; Load the colour table into the oPalette
      *info.oPaletteRef -> LoadCT, info.colourTable
    ENDELSE

  ENDELSE


  ;; Redraw the fractal using the updated colour table
  *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef

  ;; Send the info structure back in the uvalue of the button widget.
  WIDGET_CONTROL, Event.top, SET_UVALUE=info, /NO_COPY

END


;; This procedure enables the user to specify the coordinate
;; range to use for the calculation.
PRO fractalSpecifyArea, Event

  Compile_Opt IDL2 ;; Force all integer variables to be LONG and not SHORT. Force [] for arrays

  WIDGET_CONTROL, Event.top, GET_UVALUE=info, /NO_COPY

  ;; Change max iterations by changing info.numIter
  desc=[ $
    '1, BASE,, COLUMN, FRAME', $
    '1, BASE,, ROW', $
    '0, FLOAT,' + STRING(info.rMin) + ', LABEL_LEFT=Real min:, WIDTH=10, TAG=rMin', $
    '2, FLOAT,' + STRING(info.rMax) + ', LABEL_LEFT=max:, WIDTH=10, TAG=rMax', $
    '1, BASE,, ROW', $
    '0, FLOAT,' + STRING(info.iMin) + ', LABEL_LEFT=Imag min:, WIDTH=10, TAG=iMin', $
    '2, FLOAT,' + STRING(info.iMax) + ', LABEL_LEFT=max:, WIDTH=10, TAG=iMax', $
    '1, BASE,, ROW', $
    '0, BUTTON, OK, QUIT, TAG=OK', $
    '2, BUTTON, Cancel, QUIT']
  desc=[desc]

  ;; Display the form
  form = CW_FORM(desc,TITLE='Specify a fractal area', TAB_MODE=1)

  ;; If the user clicked on "OK" then
  IF form.OK EQ 1 THEN BEGIN

    ;; Update the maximum number of iterations
    info.rMin = form.rMin
    info.rMax = form.rMax
    info.iMin = form.iMin
    info.iMax = form.iMax

    ;; Recalculate the fractal at the new position, and then draw it on the screen.
    *info.theFractal -> Change, rMin=info.rMin, rMax=info.rMax, iMin=info.iMin, iMax=info.iMax

    IF info.viewIter EQ 1 THEN BEGIN
      FOR i=0, info.numIter-1 DO BEGIN
        *info.theFractal -> Iterate
        *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
      ENDFOR
    ENDIF ELSE BEGIN
      *info.theFractal -> Iterate, info.numIter
      *info.theFractal -> Draw, oImageRef=info.oImageRef, oWindowRef=info.oDrawRef, oViewRef=info.oViewRef
    ENDELSE

  ENDIF

  ;; Send the info structure back in the uvalue of the button widget.
  WIDGET_CONTROL, Event.top, SET_UVALUE=info, /NO_COPY

END


;; Empty stub procedure used for autoloading.
PRO fractal_eventcb
END
